package com.mtons.mblog.modules.service.impl;

import com.mtons.mblog.base.lang.Consts;
import com.mtons.mblog.base.utils.BeanMapUtils;
import com.mtons.mblog.base.utils.MarkdownUtils;
import com.mtons.mblog.base.utils.PreviewTextUtils;
import com.mtons.mblog.base.utils.ResourceLock;
import com.mtons.mblog.modules.aspect.PostStatusFilter;
import com.mtons.mblog.modules.data.AnswerVO;
import com.mtons.mblog.modules.data.PostVO;
import com.mtons.mblog.modules.data.QuestVO;
import com.mtons.mblog.modules.data.UserVO;
import com.mtons.mblog.modules.entity.*;
import com.mtons.mblog.modules.event.PostUpdateEvent;
import com.mtons.mblog.modules.repository.*;
import com.mtons.mblog.modules.service.AnswerFavoriteService;
import com.mtons.mblog.modules.service.AnswerService;
import com.mtons.mblog.modules.service.QuestService;
import com.mtons.mblog.modules.service.UserService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.persistence.criteria.Predicate;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Service
@Transactional
public class AnswerServiceImpl implements AnswerService {

    @Autowired
    private AnswerRepository answerRepository;
    @Autowired
    private UserService userService;
    @Autowired
    private QuestService questService;
    @Autowired
    private QuestRepository questRepository;
    @Autowired
    private AnswerAttributeRepository answerAttributeRepository;
    @Autowired
    private ResourceRepository resourceRepository;
    @Autowired
    private AnswerResourceRepository answerResourceRepository;
    @Autowired
    private ApplicationContext applicationContext;
    @Autowired
    private AnswerFavoriteService answerFavoriteService;

    private static Pattern pattern = Pattern.compile("(?<=/_signature/)(.+?)(?=\\.)");

    @Override
    @PostStatusFilter
    public Page<AnswerVO> paging(Pageable pageable, long questId, Set<Long> excludeQuestIds) {
        Page<Answer> page = answerRepository.findAll((root, query, builder) -> {
            Predicate predicate = builder.conjunction();

            if (questId > Consts.ZERO) {
                predicate.getExpressions().add(
                        builder.equal(root.get("questId").as(Long.class), questId));
            }

            if (null != excludeQuestIds && !excludeQuestIds.isEmpty()) {
                predicate.getExpressions().add(
                        builder.not(root.get("questId").in(excludeQuestIds)));
            }

//			predicate.getExpressions().add(
//					builder.equal(root.get("featured").as(Integer.class), Consts.FEATURED_DEFAULT));

            return predicate;
        }, pageable);

        return new PageImpl<>(toAnswer(page.getContent()), pageable, page.getTotalElements());
    }

    private List<AnswerVO> toAnswer(List<Answer> answers) {
        HashSet<Long> uids = new HashSet<>();
        HashSet<Long> groupIds = new HashSet<>();

        List<AnswerVO> rets = answers
                .stream()
                .map(an -> {
                    uids.add(an.getAuthorId());
                    groupIds.add(an.getQuestId());
                    return BeanMapUtils.copy(an);
                })
                .collect(Collectors.toList());

        // 加载用户信息
        buildUsers(rets, uids);
        buildGroups(rets, groupIds);

        return rets;
    }

    private void buildUsers(Collection<AnswerVO> answers, Set<Long> uids) {
        Map<Long, UserVO> userMap = userService.findMapByIds(uids);
        answers.forEach(p -> p.setAuthor(userMap.get(p.getAuthorId())));
    }

    private void buildGroups(Collection<AnswerVO> answers, Set<Long> groupIds) {
        Map<Long, QuestVO> map = questService.findMapByIds(groupIds);
        answers.forEach(p -> p.setQuest(map.get(p.getQuestId())));
    }

    @Override
    public Page<AnswerVO> paging4Admin(Pageable pageable, long questId, String title) {
        Page<Answer> page = answerRepository.findAll((root, query, builder) -> {
            Predicate predicate = builder.conjunction();
            if (questId > Consts.ZERO) {
                predicate.getExpressions().add(
                        builder.equal(root.get("questId").as(Long.class), questId));
            }
            if (StringUtils.isNotBlank(title)) {
                predicate.getExpressions().add(
                        builder.like(root.get("title").as(String.class), "%" + title + "%"));
            }
            return predicate;
        }, pageable);

        return new PageImpl<>(toAnswer(page.getContent()), pageable, page.getTotalElements());
    }

    @Override
    public Page<AnswerVO> paging4Admin(Pageable pageable, String content) {
        Page<Answer> page = answerRepository.findAll((root, query, builder) -> {
            Predicate predicate = builder.conjunction();
            if (StringUtils.isNotBlank(content)) {
                predicate.getExpressions().add(
                        builder.like(root.get("summary").as(String.class), "%" + content + "%"));
            }
            return predicate;
        }, pageable);

        return new PageImpl<>(toAnswer(page.getContent()), pageable, page.getTotalElements());
    }

    @Override
    @PostStatusFilter
    public Page<AnswerVO> pagingByAuthorId(Pageable pageable, long userId) {
        Page<Answer> page = answerRepository.findAllByAuthorId(pageable, userId);
        return new PageImpl<>(toAnswer(page.getContent()), pageable, page.getTotalElements());
    }

    @Override
    @PostStatusFilter
    public List<AnswerVO> findLatestAnswer(int maxResults) {
        return find("created", maxResults).stream().map(BeanMapUtils::copy).collect(Collectors.toList());
    }

    @PostStatusFilter
    private List<Answer> find(String orderBy, int size) {
        Pageable pageable = PageRequest.of(0, size, Sort.by(Sort.Direction.DESC, orderBy));

        Set<Long> excludeQuestIds = new HashSet<>();

        List<Quest> quests = questService.findAll(Consts.STATUS_CLOSED);
        if (quests != null) {
            quests.forEach((c) -> excludeQuestIds.add(c.getId()));
        }

        Page<Answer> page = answerRepository.findAll((root, query, builder) -> {
            Predicate predicate = builder.conjunction();
            if (excludeQuestIds.size() > 0) {
                predicate.getExpressions().add(
                        builder.not(root.get("questId").in(excludeQuestIds)));
            }
            return predicate;
        }, pageable);
        return page.getContent();
    }

    @PostStatusFilter
    private List<Answer> find(String orderBy, int size, long questId, long excludeAnswerId) {
        Pageable pageable = PageRequest.of(0, size, Sort.by(Sort.Direction.DESC, orderBy));

        Set<Long> excludeAnswerIds = new HashSet<>();

        Answer answer = answerRepository.findById(excludeAnswerId).get();
        if (answer != null) {
            excludeAnswerIds.add(answer.getId());
        }

        Page<Answer> page = answerRepository.findAll((root, query, builder) -> {
            Predicate predicate = builder.conjunction();
            if (excludeAnswerId > 0) {
                predicate.getExpressions().add(
                        builder.not(root.get("id").in(excludeAnswerIds)));
            }
            if (questId > 0) {
                predicate.getExpressions().add(builder.equal(root.get("questId"), questId));
            }
            return predicate;
        }, pageable);
        return page.getContent();
    }

    @Override
    @PostStatusFilter
    public List<AnswerVO> findHottestAnswer(int maxResults) {
        return find("views", maxResults).stream().map(BeanMapUtils::copy).collect(Collectors.toList());
    }

    @Override
    @PostStatusFilter
    public List<AnswerVO> findHottestAnswerByQustId(long questId, int maxResults, long excludeAnswerId) {
        return find("views", maxResults, questId, excludeAnswerId).stream().map(BeanMapUtils::copy).collect(Collectors.toList());
    }

    @Override
    @PostStatusFilter
    public Map<Long, AnswerVO> findMapByIds(Set<Long> ids) {
        if (ids == null || ids.isEmpty()) {
            return Collections.emptyMap();
        }

        List<Answer> list = answerRepository.findAllById(ids);
        Map<Long, AnswerVO> rets = new HashMap<>();

        HashSet<Long> uids = new HashSet<>();

        list.forEach(an -> {
            rets.put(an.getId(), BeanMapUtils.copy(an));
            uids.add(an.getAuthorId());
        });

        // 加载用户信息
        buildUsers(rets.values(), uids);
        return rets;
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public long post(AnswerVO answer) {
        Answer an = new Answer();

        BeanUtils.copyProperties(answer, an);

        an.setCreated(new Date());
        an.setStatus(answer.getStatus());

        // 处理摘要
        if (StringUtils.isBlank(answer.getSummary())) {
            an.setSummary(trimSummary(answer.getEditor(), answer.getContent()));
        } else {
            an.setSummary(answer.getSummary());
        }

        answerRepository.save(an);

        String key = ResourceLock.getAnswerKey(an.getId());
        AtomicInteger lock = ResourceLock.getAtomicInteger(key);
        try {
            synchronized (lock){
                AnswerAttribute attr = new AnswerAttribute();
                attr.setContent(answer.getContent());
                attr.setEditor(answer.getEditor());
                attr.setId(an.getId());
                answerAttributeRepository.save(attr);
                questService.identityAnswers(answer.getQuestId());
                countResource(an.getId(), null,  attr.getContent());
                onPushEvent(an, PostUpdateEvent.ACTION_PUBLISH);
                return an.getId();
            }
        }finally {
            ResourceLock.giveUpAtomicInteger(key);
        }

    }

    private void onPushEvent(Answer answer, int action) {
        PostUpdateEvent event = new PostUpdateEvent(System.currentTimeMillis());
        event.setPostId(answer.getId());
        event.setUserId(answer.getAuthorId());
        event.setAction(action);
        applicationContext.publishEvent(event);
    }

    private void countResource(Long answerId, String originContent, String newContent){
        if (StringUtils.isEmpty(originContent)){
            originContent = "";
        }
        if (StringUtils.isEmpty(newContent)){
            newContent = "";
        }

        Set<String> exists = extractImageMd5(originContent);
        Set<String> news = extractImageMd5(newContent);

        List<String> adds = ListUtils.removeAll(news, exists);
        List<String> deleteds = ListUtils.removeAll(exists, news);

        if (adds.size() > 0) {
            List<Resource> resources = resourceRepository.findByMd5In(adds);

            List<AnswerResource> ars = resources.stream().map(n -> {
                AnswerResource ar = new AnswerResource();
                ar.setResourceId(n.getId());
                ar.setAnswerId(answerId);
                ar.setPath(n.getPath());
                return ar;
            }).collect(Collectors.toList());
            answerResourceRepository.saveAll(ars);

            resourceRepository.updateAmount(adds, 1);
        }

        if (deleteds.size() > 0) {
            List<Resource> resources = resourceRepository.findByMd5In(deleteds);
            List<Long> rids = resources.stream().map(Resource::getId).collect(Collectors.toList());
            answerResourceRepository.deleteByAnswerIdAndResourceIdIn(answerId, rids);
            resourceRepository.updateAmount(deleteds, -1);
        }
    }

    private Set<String> extractImageMd5(String text) {
//		Pattern pattern = Pattern.compile("(?<=/_signature/)[^/]+?jpg");

        Set<String> md5s = new HashSet<>();

        Matcher originMatcher = pattern.matcher(text);
        while (originMatcher.find()) {
            String key = originMatcher.group();
//			md5s.add(key.substring(0, key.lastIndexOf(".")));
            md5s.add(key);
        }

        return md5s;
    }

    /**
     * 截取回答内容
     * @param text
     * @return
     */
    private String trimSummary(String editor, final String text){
        if (Consts.EDITOR_MARKDOWN.endsWith(editor)) {
            return PreviewTextUtils.getText(MarkdownUtils.renderMarkdown(text), 126);
        } else {
            return PreviewTextUtils.getText(text, 126);
        }
    }

    @Override
    public AnswerVO get(long id) {
        Optional<Answer> an = answerRepository.findById(id);
        if (an.isPresent()) {
            AnswerVO d = BeanMapUtils.copy(an.get());

            d.setAuthor(userService.get(d.getAuthorId()));
            d.setQuest(questService.get(d.getQuestId()));

            AnswerAttribute attr = answerAttributeRepository.findById(d.getId()).get();
            d.setContent(attr.getContent());
            d.setEditor(attr.getEditor());
            return d;
        }
        return null;
    }

    /**
     * 更新回答方法
     * @param a
     */
    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void update(AnswerVO a) {
        Optional<Answer> optional = answerRepository.findById(a.getId());

        if (optional.isPresent()) {
            String key = ResourceLock.getAnswerKey(a.getId());
            AtomicInteger lock = ResourceLock.getAtomicInteger(key);
            try {
                synchronized (lock){
                    Answer an = optional.get();
                    an.setTitle(a.getTitle());//标题
                    an.setQuestId(a.getQuestId());
                    an.setStatus(a.getStatus());

                    // 处理摘要
                    if (StringUtils.isBlank(a.getSummary())) {
                        an.setSummary(trimSummary(a.getEditor(), a.getContent()));
                    } else {
                        an.setSummary(a.getSummary());
                    }


                    // 保存扩展
                    Optional<AnswerAttribute> attributeOptional = answerAttributeRepository.findById(an.getId());
                    String originContent = "";
                    if (attributeOptional.isPresent()){
                        originContent = attributeOptional.get().getContent();
                    }
                    AnswerAttribute attr = new AnswerAttribute();
                    attr.setContent(a.getContent());
                    attr.setEditor(a.getEditor());
                    attr.setId(an.getId());
                    answerAttributeRepository.save(attr);

                    countResource(an.getId(), originContent, a.getContent());
                }
            }finally {
                ResourceLock.giveUpAtomicInteger(key);
            }
        }
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void updateFeatured(long id, int featured) {
        Answer an = answerRepository.findById(id).get();
        int status = Consts.FEATURED_ACTIVE == featured ? Consts.FEATURED_ACTIVE: Consts.FEATURED_DEFAULT;
        an.setFeatured(status);
        answerRepository.save(an);
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void updateWeight(long id, int weighted) {
        Answer an = answerRepository.findById(id).get();

        int max = Consts.ZERO;
        if (Consts.FEATURED_ACTIVE == weighted) {
            max = answerRepository.maxWeight() + 1;
        }
        an.setWeight(max);
        answerRepository.save(an);
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void delete(long id, long authorId) {
        Answer an = answerRepository.findById(id).get();
        // 判断文章是否属于当前登录用户
        Assert.isTrue(an.getAuthorId() == authorId, "认证失败");

        String key = ResourceLock.getAnswerKey(an.getId());
        AtomicInteger lock = ResourceLock.getAtomicInteger(key);
        try	{
            synchronized (lock){
                answerRepository.deleteById(id);
                answerAttributeRepository.deleteById(id);
                cleanResource(an.getId());
                onPushEvent(an, PostUpdateEvent.ACTION_DELETE);
            }
        }finally {
            ResourceLock.giveUpAtomicInteger(key);
        }
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void deleteByQuest(long questId) {
        List<Answer> answers = answerRepository.findByQuestId(questId);
        List<Long> idList = new ArrayList<>();
        for (Answer answer : answers) {
            idList.add(answer.getId());
        }
        delete(idList);
    }

    private void cleanResource(long answerId) {
        List<AnswerResource> list = answerResourceRepository.findByAnswerId(answerId);
        if (null == list || list.isEmpty()) {
            return;
        }
        List<Long> rids = list.stream().map(AnswerResource::getResourceId).collect(Collectors.toList());
        resourceRepository.updateAmountByIds(rids, -1);
        answerResourceRepository.deleteByAnswerId(answerId);
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void delete(Collection<Long> ids) {
        if (CollectionUtils.isNotEmpty(ids)) {
            List<Answer> list = answerRepository.findAllById(ids);
            list.forEach(an -> {
                String key = ResourceLock.getAnswerKey(an.getId());
                AtomicInteger lock = ResourceLock.getAtomicInteger(key);
                try	{
                    synchronized (lock){
                        answerRepository.delete(an);
                        answerAttributeRepository.deleteById(an.getId());
                        cleanResource(an.getId());
                        onPushEvent(an, PostUpdateEvent.ACTION_DELETE);
                    }
                }finally {
                    ResourceLock.giveUpAtomicInteger(key);
                }
            });
        }
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void identityViews(long id) {
        // 不清理缓存, 等待文章缓存自动过期
        answerRepository.updateViews(id, Consts.IDENTITY_STEP);
    }

    @Override
    @Transactional
    public void identityComments(long id) {
        answerRepository.updateComments(id, Consts.IDENTITY_STEP);
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void favor(long userId, long answerId) {
        answerRepository.updateFavors(answerId, Consts.IDENTITY_STEP);
        answerFavoriteService.add(userId, answerId);
    }

    @Override
    @Transactional(rollbackFor = Throwable.class)
    public void unfavor(long userId, long answerId) {
        answerRepository.updateFavors(answerId,  Consts.DECREASE_STEP);
        answerFavoriteService.delete(userId, answerId);
    }

    @Override
    @PostStatusFilter
    public long count() {
        return answerRepository.count();
    }
}
